package com.wimcorp.magic.abilities;

import java.rmi.RemoteException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.ElementCollection;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.lang.StringUtils;

import com.wimcorp.magic.abilities.trigger.AbilityTrigger;
import com.wimcorp.magic.board.CardAccessor;
import com.wimcorp.magic.cards.Creature;
import com.wimcorp.magic.cards.PlayingCard;
import com.wimcorp.magic.cost.Cost;
import com.wimcorp.magic.cost.CostHolder;
import com.wimcorp.magic.exception.CastException;
import com.wimcorp.magic.exception.CostException;
import com.wimcorp.magic.input.Duration;
import com.wimcorp.magic.input.Target;
import com.wimcorp.magic.players.Player;
import com.wimcorp.magic.spell.Castable;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "condition_type", discriminatorType = DiscriminatorType.INTEGER)
@Table(name = "ability", schema = "abilities")
public abstract class Ability implements Castable {

	private static long identifierCounter = 1;

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	@Transient
	private long identifier;

	@Transient
	private PlayingCard card;

	@Transient
	private long input;

	@Transient
	private PlayingCard abilityTarget;

	@Embedded
	private CostHolder costHolder = new CostHolder();

	@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	private AbilityTrigger trigger;

	@Column(columnDefinition = "text")
	private String description;

	@Enumerated(EnumType.STRING)
	private Target target;

	@ElementCollection(fetch = FetchType.EAGER)
	@CollectionTable(name = "ability_types", schema = "abilities", joinColumns = @JoinColumn(name = "addbehavior_id"))
	private Set<String> types = new HashSet<>();

	@ElementCollection(fetch = FetchType.EAGER)
	@CollectionTable(name = "ability_locations", schema = "abilities", joinColumns = @JoinColumn(name = "addbehavior_id"))
	private Set<String> locations = new HashSet<>();

	@ElementCollection(fetch = FetchType.EAGER)
	@CollectionTable(name = "ability_behaviors", schema = "abilities", joinColumns = @JoinColumn(name = "addbehavior_id"))
	private Set<String> behaviors = new HashSet<>();

	@Enumerated(EnumType.STRING)
	private Duration duration;

	protected Ability() {
	}

	public Ability(AbilityTrigger trigger) {
		setTrigger(trigger);
	}

	public Ability(Ability ability) {
		this.identifier = identifierCounter++;
		this.card = ability.getCard();
		this.description = ability.getDescription();
		this.target = ability.getTarget();
		this.duration = ability.getDuration();
		this.behaviors = ability.getBehaviors();
		this.locations = ability.getLocations();
		this.types = ability.getTypes();
		for (Cost cost : ability.getCosts()) {
			cost.setCastable(card);
			this.costHolder.addCost(cost.clone());
		}
		
		AbilityTrigger clonedTrigger = ability.getTrigger().clone();
		clonedTrigger.setAbility(this);
		setTrigger(clonedTrigger);
	}

	public abstract Ability clone();

	protected abstract void executeAbility() throws CastException;

	public void cast() throws Exception {
		executeAbility();
	}

	public void uncast() {
	}

	public long getInput() {
		return input;
	}

	public void configureTarget() throws RemoteException {
		switch (target) {
		case THIS:
			input = getCard().getIdentifier();
		default: 
			getPlayer().message(getDescription());
			input = getPlayer().getUserInterface().select(target, new HashSet<>(types), new HashSet<>(locations),
					new HashSet<>(behaviors));
		}
		if (input > 0) {
			PlayingCard card = getAbilityTarget();
			for (Ability ability : card.getAbilityTargetObservers()) {
				getPlayer().cast(ability);
			}
		}
	}

	public CardAccessor getCardAccesor(long id, String location) {

		CardAccessor cardAccessor = null;
		switch (target) {
		case SELF:
			cardAccessor = CardAccessor.getCardAccessor(getPlayer(), location);
			break;
		case OPPONENT:
			cardAccessor = CardAccessor.getCardAccessor(getPlayer().getOpponent(), location);
			break;
		case ANY:
			PlayingCard card = null;
			cardAccessor = CardAccessor.getCardAccessor(getPlayer(), location);
			card = (Creature) cardAccessor.getCard(id);
			if (card == null) {
				cardAccessor = CardAccessor.getCardAccessor(getPlayer().getOpponent(), location);
				card = (Creature) cardAccessor.getCard(id);
			}
			break;
		}
		return cardAccessor;
	}

	protected void registerUndo() {
		switch (duration) {
		case END_OF_TURN:
			getPlayer().getEndingPhase().registerEndOfTurnObserver(new UndoAbility(this));
			break;
		}

	}

	public void addCost(Cost cost) {
		this.costHolder.addCost(cost);
	}

	public void removeCost(Cost cost) {
		this.costHolder.removeCost(cost);
	}

	public void payCost() throws CostException {
		costHolder.payCost();
	}

	public void configure() throws RemoteException {
		if (isInputRequired()) {
			configureTarget();
			if (abilityTarget != null) {
				getPlayer().messageOpponent(
						String.format("Opponent %s played %s with target %s", getPlayer().getName(), getDescription(),
								abilityTarget.getName()));
			}else{
				getPlayer().message(String.format("You failed to select a target for %s.", card.getName()));
			}
		} else {
			getPlayer().messageOpponent(String.format("Opponent %s played %s ", getPlayer().getName(), card.getName()));
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("		id : " + identifier);
		sb.append("\n");
		sb.append("		" + description);
		/*if (costHolder.size() > 0 && StringUtils.isNotEmpty(costHolder.toString())) {
			sb.append(costHolder);
			sb.append("\n");
		}*/
		return sb.toString();
	}

	@Override
	public String getCastDescription() {
		return getDescription();
	}

	public PlayingCard getAbilityTarget() {
		if (abilityTarget == null) {
			if (Target.THIS.equals(getTarget())) {
				abilityTarget = getCard();
			} else {
				for (String location : locations) {
					if (abilityTarget == null) {
						CardAccessor cardAccessor = getCardAccesor(input, location);
						abilityTarget = cardAccessor.getCard(input);
					}
				}
			}
		}
		return abilityTarget;
	}

	public void setAbilityTarget(PlayingCard abilityTarget) {
		this.abilityTarget = abilityTarget;
	}

	public void setTrigger(AbilityTrigger trigger) {
		this.trigger = trigger;
		if (trigger.getAbility() == null) {
			trigger.setAbility(this);
		}
	}

	public boolean isInputRequired() {
		return false;
	}

	public long getIdentifier() {
		return identifier;
	}

	public AbilityTrigger getTrigger() {
		return trigger;
	}

	public PlayingCard getCard() {
		return card;
	}

	public void setCard(PlayingCard card) {
		this.card = card;
	}

	public Player getPlayer() {
		return card.getPlayer();
	}

	public String getDescription() {
		return description;
	}

	public Target getTarget() {
		return target;
	}

	public Set<String> getTypes() {
		return types;
	}

	public Set<String> getLocations() {
		return locations;
	}

	public Set<String> getBehaviors() {
		return behaviors;
	}

	public Duration getDuration() {
		return duration;
	}

	public void setTarget(Target target) {
		this.target = target;
	}

	public void setDuration(Duration duration) {
		this.duration = duration;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public void setTypes(Set<String> types) {
		this.types = types;
	}

	public void setLocations(Set<String> locations) {
		this.locations = locations;
	}

	public void setBehaviors(Set<String> behaviors) {
		this.behaviors = behaviors;
	}

	public List<Cost> getCosts() {
		return costHolder.getCosts();
	}

	public void setIdentifier(long identifier) {
		this.identifier = identifier;
	}

}
